/*
 * Copyright 2010 Srikanth Reddy Lingala
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.lingala.zip4j.zip;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.io.SplitOutputStream;
import net.lingala.zip4j.io.ZipOutputStream;
import net.lingala.zip4j.model.EndCentralDirRecord;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipModel;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.progress.ProgressMonitor;
import net.lingala.zip4j.util.ArchiveMaintainer;
import net.lingala.zip4j.util.CRCUtil;
import net.lingala.zip4j.util.InternalZipConstants;
import net.lingala.zip4j.util.Zip4jConstants;
import net.lingala.zip4j.util.Zip4jUtil;

public class ZipEngine {

    private ZipModel zipModel;

    public ZipEngine(ZipModel zipModel) throws ZipException {

        if (zipModel == null) {
            throw new ZipException("zip model is null in ZipEngine constructor");
        }

        this.zipModel = zipModel;
    }

    public void addFiles(final ArrayList fileList, final ZipParameters parameters,
                         final ProgressMonitor progressMonitor, boolean runInThread) throws ZipException {

        if (fileList == null || parameters == null) {
            throw new ZipException("one of the input parameters is null when adding files");
        }

        if (fileList.size() <= 0) {
            throw new ZipException("no files to add");
        }

        progressMonitor.setTotalWork(calculateTotalWork(fileList, parameters));
        progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_ADD);
        progressMonitor.setState(ProgressMonitor.STATE_BUSY);
        progressMonitor.setResult(ProgressMonitor.RESULT_WORKING);

        if (runInThread) {

            Thread thread = new Thread(InternalZipConstants.THREAD_NAME) {
                public void run() {
                    try {
                        initAddFiles(fileList, parameters, progressMonitor);
                    } catch (ZipException e) {
                    }
                }
            };
            thread.start();

        } else {
            initAddFiles(fileList, parameters, progressMonitor);
        }
    }

    private void initAddFiles(ArrayList fileList, ZipParameters parameters,
                              ProgressMonitor progressMonitor) throws ZipException {

        if (fileList == null || parameters == null) {
            throw new ZipException("one of the input parameters is null when adding files");
        }

        if (fileList.size() <= 0) {
            throw new ZipException("no files to add");
        }

        if (zipModel.getEndCentralDirRecord() == null) {
            zipModel.setEndCentralDirRecord(createEndOfCentralDirectoryRecord());
        }

        ZipOutputStream outputStream = null;
        InputStream inputStream = null;
        try {
            checkParameters(parameters);

            removeFilesIfExists(fileList, parameters, progressMonitor);

            boolean isZipFileAlreadExists = Zip4jUtil.checkFileExists(zipModel.getZipFile());

            SplitOutputStream splitOutputStream = new SplitOutputStream(new File(zipModel.getZipFile()), zipModel.getSplitLength());
            outputStream = new ZipOutputStream(splitOutputStream, this.zipModel);

            if (isZipFileAlreadExists) {
                if (zipModel.getEndCentralDirRecord() == null) {
                    throw new ZipException("invalid end of central directory record");
                }
                splitOutputStream.seek(zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir());
            }
            byte[] readBuff = new byte[InternalZipConstants.BUFF_SIZE];
            int readLen = -1;
            for (int i = 0; i < fileList.size(); i++) {
                ZipParameters fileParameters = (ZipParameters) parameters.clone();

                progressMonitor.setFileName(((File) fileList.get(i)).getAbsolutePath());

                if (!((File) fileList.get(i)).isDirectory()) {
                    if (fileParameters.isEncryptFiles() && fileParameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) {
                        progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_CALC_CRC);
                        fileParameters.setSourceFileCRC((int) CRCUtil.computeFileCRC(((File) fileList.get(i)).getAbsolutePath(), progressMonitor));
                        progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_ADD);
                    }

                    if (Zip4jUtil.getFileLengh((File) fileList.get(i)) == 0) {
                        fileParameters.setCompressionMethod(Zip4jConstants.COMP_STORE);
                    }
                }

                outputStream.putNextEntry((File) fileList.get(i), fileParameters);
                if (((File) fileList.get(i)).isDirectory()) {
                    outputStream.closeEntry();
                    continue;
                }

                inputStream = new FileInputStream((File) fileList.get(i));

                while ((readLen = inputStream.read(readBuff)) != -1) {
                    outputStream.write(readBuff, 0, readLen);
                    progressMonitor.updateWorkCompleted(readLen);
                }

                outputStream.closeEntry();

                if (inputStream != null) {
                    inputStream.close();
                }
            }

            outputStream.finish();
            progressMonitor.endProgressMonitorSuccess();
        } catch (ZipException e) {
            progressMonitor.endProgressMonitorError(e);
            throw e;
        } catch (Exception e) {
            progressMonitor.endProgressMonitorError(e);
            throw new ZipException(e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                }
            }

            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                }
            }
        }
    }

    public void addStreamToZip(InputStream inputStream, ZipParameters parameters) throws ZipException {
        if (inputStream == null || parameters == null) {
            throw new ZipException("one of the input parameters is null, cannot add stream to zip");
        }

        ZipOutputStream outputStream = null;

        try {
            checkParameters(parameters);

            boolean isZipFileAlreadExists = Zip4jUtil.checkFileExists(zipModel.getZipFile());

            SplitOutputStream splitOutputStream = new SplitOutputStream(new File(zipModel.getZipFile()), zipModel.getSplitLength());
            outputStream = new ZipOutputStream(splitOutputStream, this.zipModel);

            if (isZipFileAlreadExists) {
                if (zipModel.getEndCentralDirRecord() == null) {
                    throw new ZipException("invalid end of central directory record");
                }
                splitOutputStream.seek(zipModel.getEndCentralDirRecord().getOffsetOfStartOfCentralDir());
            }

            byte[] readBuff = new byte[InternalZipConstants.BUFF_SIZE];
            int readLen = -1;

            outputStream.putNextEntry(null, parameters);

            if (!parameters.getFileNameInZip().endsWith("/") &&
                    !parameters.getFileNameInZip().endsWith("\\")) {
                while ((readLen = inputStream.read(readBuff)) != -1) {
                    outputStream.write(readBuff, 0, readLen);
                }
            }

            outputStream.closeEntry();
            outputStream.finish();

        } catch (ZipException e) {
            throw e;
        } catch (Exception e) {
            throw new ZipException(e);
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    //ignore
                }
            }
        }
    }

    public void addFolderToZip(File file, ZipParameters parameters,
                               ProgressMonitor progressMonitor, boolean runInThread) throws ZipException {
        if (file == null || parameters == null) {
            throw new ZipException("one of the input parameters is null, cannot add folder to zip");
        }

        if (!Zip4jUtil.checkFileExists(file.getAbsolutePath())) {
            throw new ZipException("input folder does not exist");
        }

        if (!file.isDirectory()) {
            throw new ZipException("input file is not a folder, user addFileToZip method to add files");
        }

        if (!Zip4jUtil.checkFileReadAccess(file.getAbsolutePath())) {
            throw new ZipException("cannot read folder: " + file.getAbsolutePath());
        }

        String rootFolderPath = null;
        if (parameters.isIncludeRootFolder()) {
            if (file.getAbsolutePath() != null) {
                rootFolderPath = file.getAbsoluteFile().getParentFile() != null ? file.getAbsoluteFile().getParentFile().getAbsolutePath() : "";
            } else {
                rootFolderPath = file.getParentFile() != null ? file.getParentFile().getAbsolutePath() : "";
            }
        } else {
            rootFolderPath = file.getAbsolutePath();
        }

        parameters.setDefaultFolderPath(rootFolderPath);

        ArrayList fileList = Zip4jUtil.getFilesInDirectoryRec(file, parameters.isReadHiddenFiles());

        if (parameters.isIncludeRootFolder()) {
            if (fileList == null) {
                fileList = new ArrayList();
            }
            fileList.add(file);
        }

        addFiles(fileList, parameters, progressMonitor, runInThread);
    }


    private void checkParameters(ZipParameters parameters) throws ZipException {

        if (parameters == null) {
            throw new ZipException("cannot validate zip parameters");
        }

        if ((parameters.getCompressionMethod() != Zip4jConstants.COMP_STORE) &&
                parameters.getCompressionMethod() != Zip4jConstants.COMP_DEFLATE) {
            throw new ZipException("unsupported compression type");
        }

        if (parameters.getCompressionMethod() == Zip4jConstants.COMP_DEFLATE) {
            if (parameters.getCompressionLevel() < 0 && parameters.getCompressionLevel() > 9) {
                throw new ZipException("invalid compression level. compression level dor deflate should be in the range of 0-9");
            }
        }

        if (parameters.isEncryptFiles()) {
            if (parameters.getEncryptionMethod() != Zip4jConstants.ENC_METHOD_STANDARD &&
                    parameters.getEncryptionMethod() != Zip4jConstants.ENC_METHOD_AES) {
                throw new ZipException("unsupported encryption method");
            }

            if (parameters.getPassword() == null || parameters.getPassword().length <= 0) {
                throw new ZipException("input password is empty or null");
            }
        } else {
            parameters.setAesKeyStrength(-1);
            parameters.setEncryptionMethod(-1);
        }

    }

    /**
     * Before adding a file to a zip file, we check if a file already exists in the zip file
     * with the same fileName (including path, if exists). If yes, then we remove this file
     * before adding the file<br><br>
     *
     * <b>Note:</b> Relative path has to be passed as the fileName
     *
     * @param zipModel
     * @param fileName
     * @throws ZipException
     */
    private void removeFilesIfExists(ArrayList fileList, ZipParameters parameters, ProgressMonitor progressMonitor) throws ZipException {

        if (zipModel == null || zipModel.getCentralDirectory() == null ||
                zipModel.getCentralDirectory().getFileHeaders() == null ||
                zipModel.getCentralDirectory().getFileHeaders().size() <= 0) {
            //For a new zip file, this condition satisfies, so do nothing
            return;
        }
        RandomAccessFile outputStream = null;

        try {
            for (int i = 0; i < fileList.size(); i++) {
                File file = (File) fileList.get(i);

                String fileName = Zip4jUtil.getRelativeFileName(file.getAbsolutePath(),
                        parameters.getRootFolderInZip(), parameters.getDefaultFolderPath());

                FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, fileName);
                if (fileHeader != null) {

                    if (outputStream != null) {
                        outputStream.close();
                        outputStream = null;
                    }

                    ArchiveMaintainer archiveMaintainer = new ArchiveMaintainer();
                    progressMonitor.setCurrentOperation(ProgressMonitor.OPERATION_REMOVE);
                    HashMap retMap = archiveMaintainer.initRemoveZipFile(zipModel,
                            fileHeader, progressMonitor);

                    if (progressMonitor.isCancelAllTasks()) {
                        progressMonitor.setResult(ProgressMonitor.RESULT_CANCELLED);
                        progressMonitor.setState(ProgressMonitor.STATE_READY);
                        return;
                    }

                    progressMonitor
                            .setCurrentOperation(ProgressMonitor.OPERATION_ADD);

                    if (outputStream == null) {
                        outputStream = prepareFileOutputStream();

                        if (retMap != null) {
                            if (retMap.get(InternalZipConstants.OFFSET_CENTRAL_DIR) != null) {
                                long offsetCentralDir = -1;
                                try {
                                    offsetCentralDir = Long
                                            .parseLong((String) retMap
                                                    .get(InternalZipConstants.OFFSET_CENTRAL_DIR));
                                } catch (NumberFormatException e) {
                                    throw new ZipException(
                                            "NumberFormatException while parsing offset central directory. " +
                                                    "Cannot update already existing file header");
                                } catch (Exception e) {
                                    throw new ZipException(
                                            "Error while parsing offset central directory. " +
                                                    "Cannot update already existing file header");
                                }

                                if (offsetCentralDir >= 0) {
                                    outputStream.seek(offsetCentralDir);
                                }
                            }
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new ZipException(e);
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    //ignore
                }
            }
        }
    }

    private RandomAccessFile prepareFileOutputStream() throws ZipException {
        String outPath = zipModel.getZipFile();
        if (!Zip4jUtil.isStringNotNullAndNotEmpty(outPath)) {
            throw new ZipException("invalid output path");
        }

        try {
            File outFile = new File(outPath);
            if (!outFile.getParentFile().exists()) {
                outFile.getParentFile().mkdirs();
            }
            return new RandomAccessFile(outFile, InternalZipConstants.WRITE_MODE);
        } catch (FileNotFoundException e) {
            throw new ZipException(e);
        }
    }

    private EndCentralDirRecord createEndOfCentralDirectoryRecord() {
        EndCentralDirRecord endCentralDirRecord = new EndCentralDirRecord();
        endCentralDirRecord.setSignature(InternalZipConstants.ENDSIG);
        endCentralDirRecord.setNoOfThisDisk(0);
        endCentralDirRecord.setTotNoOfEntriesInCentralDir(0);
        endCentralDirRecord.setTotNoOfEntriesInCentralDirOnThisDisk(0);
        endCentralDirRecord.setOffsetOfStartOfCentralDir(0);
        return endCentralDirRecord;
    }

    private long calculateTotalWork(ArrayList fileList, ZipParameters parameters) throws ZipException {
        if (fileList == null) {
            throw new ZipException("file list is null, cannot calculate total work");
        }

        long totalWork = 0;

        for (int i = 0; i < fileList.size(); i++) {
            if (fileList.get(i) instanceof File) {
                if (((File) fileList.get(i)).exists()) {
                    if (parameters.isEncryptFiles() &&
                            parameters.getEncryptionMethod() == Zip4jConstants.ENC_METHOD_STANDARD) {
                        totalWork += (Zip4jUtil.getFileLengh((File) fileList.get(i)) * 2);
                    } else {
                        totalWork += Zip4jUtil.getFileLengh((File) fileList.get(i));
                    }

                    if (zipModel.getCentralDirectory() != null &&
                            zipModel.getCentralDirectory().getFileHeaders() != null &&
                            zipModel.getCentralDirectory().getFileHeaders().size() > 0) {
                        String relativeFileName = Zip4jUtil.getRelativeFileName(
                                ((File) fileList.get(i)).getAbsolutePath(), parameters.getRootFolderInZip(), parameters.getDefaultFolderPath());
                        FileHeader fileHeader = Zip4jUtil.getFileHeader(zipModel, relativeFileName);
                        if (fileHeader != null) {
                            totalWork += (Zip4jUtil.getFileLengh(new File(zipModel.getZipFile())) - fileHeader.getCompressedSize());
                        }
                    }
                }
            }
        }

        return totalWork;
    }
}
